package lintulaskenta.tipuapiValidointi;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * Document used with XMLWriter and XMLReader.
 * Contains root node.
 * @see Node
 */
public class Document {

	private final Node rootNode;

	/**
	 * Create new document with given name as the name of the root element
	 * @param rootNodeName
	 */
	public Document(String rootNodeName) {
		this.rootNode = new Node(rootNodeName);
	}

	/**
	 * Get rood node
	 * @return
	 */
	public Node getRootNode() {
		return rootNode;
	}

	/**
	 * Defines an attribute
	 */
	public static class Attribute {
		private final String name;
		private final String value;

		/**
		 * Define a new attribute
		 * @param name
		 * @param value
		 */
		public Attribute(String name, String value) {
			if (name == null || name.length() < 1) {
				throw new IllegalArgumentException("Attribute's name must be given");
			}
			if (value == null) value ="";
			this.name = name.trim();
			this.value = value.trim();
		}

		public String getName() {
			return name;
		}

		public String getValue() {
			return value;
		}

		@Override
		public String toString() {
			return name + "=" + value;
		}
	}

	/**
	 * Defines a node
	 */
	public static class Node implements Iterable<Node> {
		private final String name;
		private final List<Attribute> attributes = new LinkedList<Attribute>();
		private final LinkedList<Node> childNodes = new LinkedList<Document.Node>();
		private String contents;
		private boolean contentIsXML = false;
		private boolean contentIsCDATA = false;

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder(name).append(":");
			if (this.hasAttributes()) {
				builder.append(" attributes: ").append(attributes);
			}
			if (this.hasChildNodes()) {
				builder.append(" children: ").append(childNodes);
			}
			if (this.hasContents()) {
				builder.append(" content: ").append(this.getContents());
			}
			return builder.toString();
		}
		/**
		 * Defines a new node with the given
		 * @param name
		 * @throws IllegalArgumentException if name is not given
		 */
		public Node(String name) throws IllegalArgumentException {
			if (name == null || name.length() < 1) {
				throw new IllegalArgumentException("Node's name must be given");
			}
			this.name = name.trim();
		}

		public String getName() {
			return name;
		}

		/**
		 * Returns contents of the node or empty string if not set
		 * @return
		 */
		public String getContents() {
			if (contents == null) return "";
			return contents;
		}

		/**
		 * Does node have contents?
		 * @return return contents != null
		 */
		public boolean hasContents() {
			return contents != null;
		}

		/**
		 * Is content XML? (XML content will not be trimmed or indented)
		 * @return
		 * @see contentIsXML(boolean b)
		 */
		public boolean contentIsXML() {
			return contentIsXML;
		}

		/**
		 * Is content CDATA?
		 * @return
		 */
		public boolean contentIsCDATA() {
			return contentIsCDATA;
		}

		/**
		 * Does this node have child nodes?
		 * @return
		 */
		public boolean hasChildNodes() {
			return !childNodes.isEmpty();
		}

		/**
		 * Get list of attributes (empty list if none)
		 * @return
		 */
		public List<Attribute> getAttributes() {
			return attributes;
		}

		/**
		 * Does this node have attributes?
		 * @return return !attributes.isEmpty()
		 */
		public boolean hasAttributes() {
			return !attributes.isEmpty();
		}

		/**
		 * Sets contents. If content is XML it is not trimmed, otherwise whitespice is removed around the content
		 * @param contents
		 * @return This node
		 */
		public Node setContents(String contents) {
			if (contents == null) contents = "";
			if (contentIsXML) {
				this.contents = contents;
			} else {
				this.contents = contents.trim();
			}
			return this;
		}

		/**
		 * Sets CDATA contents. CDATA content is surrounded with CDATA -tags.
		 * @param contents
		 * @return This node.
		 */
		public Node setCDATA(String contents) {
			this.contentIsCDATA = true;
			this.contents = contents;
			return this;
		}

		/**
		 * Adds a new attribute
		 * @param name
		 * @param value
		 * @return This node.
		 */
		public Node addAttribute(String name, String value) {
			this.attributes.add(new Attribute(name, value));
			return this;
		}

		/**
		 * Adds a child node.
		 * @param name
		 * @return The new node.
		 */
		public Node addChildNode(String name) {
			Node node = new Node(name);
			this.childNodes.add(node);
			return node;
		}

		/**
		 * Adds a child node.
		 * @param node
		 * @return The new node.
		 */
		public Node addChildNode(Node node) {
			this.childNodes.add(node);
			return node;
		}

		/**
		 * Defines if content is XLM or not.
		 * @param b
		 */
		public void contentIsXML(boolean b) {
			contentIsXML = b;
		}

		/**
		 * Returns value of attribute
		 * @param name
		 * @return
		 * @throws IllegalArgumentException if attribute does not exists
		 */
		public String getAttribute(String name) throws IllegalArgumentException {
			for (Attribute attribute : attributes) {
				if (attribute.getName().equals(name)) {
					return attribute.getValue();
				}
			}
			throw new IllegalArgumentException("No attribute with name " +name + " is defined");
		}

		/**
		 * Returns the first child node that matches the given name
		 * @param name
		 * @return
		 * @throws IllegalArgumentException if no child node exists with the given name
		 */
		public Node getNode(String name) throws IllegalArgumentException {
			for (Node child : this) {
				if (child.getName().equals(name)) {
					return child;
				}
			}
			throw new IllegalArgumentException("No node with name " + name + " is defined");
		}

		@Override
		public Iterator<Node> iterator() {
			return childNodes.iterator();
		}

		/**
		 * Get list of child nodes
		 * @return
		 */
		public List<Node> getChildNodes() {
			return childNodes;
		}

		/**
		 * Get list of child nodes that match the given name
		 * @param name
		 * @return empty list if no matches
		 */
		public List<Node> getChildNodes(String name) {
			List<Node> nodes = new ArrayList<Document.Node>();
			for (Node child : this) {
				if (child.getName().equals(name)) {
					nodes.add(child);
				}
			}
			return nodes;
		}

		/**
		 * Sort child nodes of this node using the given comparator
		 * @param comparator
		 */
		public void sortChildren(Comparator<Node> comparator) {
			Collections.sort(this.childNodes, comparator);
		}

		public boolean hasAttribute(String name) {
			try {
				this.getAttribute(name);
			} catch (IllegalArgumentException e) {
				return false;
			}
			return true;
		}

		public boolean hasChildNodes(String name) {
			List<Node> list = this.getChildNodes(name);
			return !list.isEmpty();
		}

	}

}
